import http from "http";
import * as url from "url";
import type * as internal from "stream";
import { SocksCommandOption } from "socks/typings/common/constants";

const { SocksProxyAgent } = require("socks-proxy-agent");
const { SocksClient } = require("socks");
const portscanner = require("portscanner");

export default class Socks5 {
  proxyHost: string;
  proxyPort: number;
  uname: string;
  passwd: string;
  _server: http.Server | undefined;
  _proxyUrl: string | undefined;

  constructor(proxyHost: string, proxyPort: number, uname: string, passwd: string) {
    this.proxyHost = proxyHost;
    this.proxyPort = proxyPort;
    this.uname = uname;
    this.passwd = passwd;
  }

  _connect(
    uReq: http.IncomingMessage,
    uSocket: internal.Duplex,
    uHead: Buffer
  ) {
    const u = url.parse(`http://${uReq.url}`);
    SocksClient.createConnection({
      proxy: {
        ipaddress: this.proxyHost,
        port: this.proxyPort,
        type: 5,
        userId: this.uname,
        password: this.passwd
      },
      destination: { host: u.hostname!, port: u.port ? +u.port! : 80 },
      command: "connect" as SocksCommandOption
    }, (error, pSocket) => {
      if (error) {
        uSocket?.write(`HTTP/${uReq.httpVersion} 500 Connection error\r\n\r\n`);
        console.log("connect:error", error);
        return;
      }
      pSocket?.socket.pipe(uSocket);
      if (pSocket?.socket) {
        uSocket?.pipe(pSocket?.socket);
      }
      pSocket?.socket.on("error", err => {
        console.log("connect:error", err);
      });
      uSocket.on("error", err => {
        console.log("connect:error", err);
      });
      pSocket?.socket.write(uHead);
      uSocket?.write(`HTTP/${uReq.httpVersion} 200 Connection established\r\n\r\n`);
      console.log("connect:success");
      pSocket?.socket.resume();
    });
  }

  _request(
    uReq: http.IncomingMessage,
    uRes: http.ServerResponse<http.IncomingMessage> & {
      req: http.IncomingMessage
    }
  ) {
    const u = url.parse(uReq.url!);
    const socksAgent = new SocksProxyAgent(
      `socks://${this.uname}:${this.passwd}@${this.proxyHost}:${this.proxyPort}`
    );

    const options = {
      hostname: u.hostname,
      port: u.port || 80,
      path: u.path,
      method: uReq.method || "get",
      headers: uReq.headers,
      agent: socksAgent
    };
    const pReq = http.request(options);
    pReq
      .on("response", (pRes) => {
        pRes.pipe(uRes);
        uRes.writeHead(pRes.statusCode!, pRes.headers);
        console.log("request:success");
      })
      .on("error", (e) => {
        uRes.writeHead(500);
        uRes.end("Connection error\n");
        console.log("request:error", e);
      });
    uReq.pipe(pReq);
  }

  async start() {
    const listenHost = "127.0.0.1";
    const listenPort = await portscanner.findAPortNotInUse(30000, 40000);
    const srv = http.createServer();
    srv.on("connect", (...args) => this._connect(...args));
    srv.on("request", (...args) => this._request(...args));
    this._proxyUrl = `http://${listenHost}:${listenPort}`;
    this._server = srv.listen(listenPort, listenHost);
    return this._proxyUrl;
  }

  close() {
    this._server?.close();
  }

  proxyUrl() {
    return this._proxyUrl;
  }
}
